﻿using System;
using System.Collections.Generic;
using System.Linq;
using Fabrication.Exceptions;
using Fabrication.Tests.TestClasses;
using NUnit.Framework;

namespace Fabrication.Tests
{
    [TestFixture]
    public class multiple_item_fabricator
    {
        [Test]
        public void should_throw_exception_if_number_of_items_is_zero()
        {
            var fabricationScope = new FabricationScope();
            Assert.Throws<ArgumentException>(() => fabricationScope.Generate<DefaultConstructor>(0).Count());
        }

        [Test]
        public void should_throw_exception_if_number_of_items_is_negative()
        {
            var fabricationScope = new FabricationScope();
            Assert.Throws<ArgumentException>(() => fabricationScope.Generate<DefaultConstructor>(-1).Count());
        }

        [Test]
        public void should_use_value_to_set_properties_if_defined()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<string>>()
                .For(x => x.Value).Use("MyName")
                .Generate(5);

            Assert.That(builtItems.All(x => x.Value == "MyName"));
        }

        [Test]
        public void should_use_rule_to_set_properties_if_defined()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<string>>()
                .For(x => x.Value).Use(x => "MyName")
                .Generate(5);

            Assert.That(builtItems.All(x => x.Value == "MyName"));
        }

        // TODO: Allow direct methods here? (i.e. Func rather than Expression)
        [Test]
        public void should_use_method_to_set_properties_if_defined()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<string>>()
                .For(x => x.Value).Use(x => GenerateValue(x))
                .Generate(5);

            Assert.That(builtItems.All(x => x.Value == "MyValue"));
        }

        private static string GenerateValue(SinglePropertyClass<string> item)
        {
            return "MyValue";
        }

        // TODO add test to ensure that a rule that relies on another value is executed
        // after the other value is populated

        [Test]
        public void should_generate_multiple_items()
        {
            var builtItems = new FabricationScope().Generate<DefaultConstructor>(10);
            
            Assert.That(builtItems.Count(), Is.EqualTo(10));
            Assert.That(builtItems.First(), Is.InstanceOf<DefaultConstructor>().And.Not.Null);
        }

        [Test]
        public void should_generate_random_number_of_items_within_specifiedrange()
        {
            var builtItems = new FabricationScope().Generate<SinglePropertyClass<string>>(10, 15);

            Assert.That(builtItems.Count(), Is.GreaterThanOrEqualTo(10).And.LessThanOrEqualTo(15));
        }

        [Test]
        public void should_share_options_between_calls_to_generate()
        {
            var values = new Queue<int>(new[] {1, 2});
            var fabricationScope = new FabricationScope();
            fabricationScope.Setup<SinglePropertyClass<int>>()
                .For(x => x.Value).Use(x => values.Dequeue());

            var builtItem1 = fabricationScope.Generate<SinglePropertyClass<int>>();
            var builtItem2 = fabricationScope.Generate<SinglePropertyClass<int>>();

            Assert.That(builtItem1.Value, Is.EqualTo(1));
            Assert.That(builtItem2.Value, Is.EqualTo(2));
        }

        [Test]
        public void should_not_share_values_between_calls_to_generate()
        {
            var builtItem1 = new FabricationScope().Generate<StringAndInteger>();
            var builtItem2 = new FabricationScope().Generate<StringAndInteger>();

            Assert.That(builtItem2.Number, Is.EqualTo(builtItem1.Number));
            Assert.That(builtItem2.Name, Is.EqualTo(builtItem1.Name));
        }

        [Test]
        public void should_use_inline_sequence()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<int>>()
                .For(x => x.Value).UseSequence(1, 2, 3)
                .Generate(3);

            var numbers = builtItems.Select(i => i.Value).ToArray();

            Assert.That(numbers, Is.EquivalentTo(new[] { 1, 2, 3 }));
        }

        [Test]
        public void should_use_enumerable_sequence()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<int>>()
                .For(x => x.Value).UseSequence(new List<int> {1, 2, 3})
                .Generate(3);

            var numbers = builtItems.Select(i => i.Value).ToArray();

            Assert.That(numbers, Is.EquivalentTo(new[] { 1, 2, 3 }));
        }

        [Test]
        public void should_use_first_sequence_values_when_generating_less_items_than_sequence_size()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<int>>()
                .For(x => x.Value).UseSequence(1, 2, 3, 4, 5)
                .Generate(2);

            var numbers = builtItems.Select(i => i.Value).ToArray();

            Assert.That(numbers, Is.EquivalentTo(new[] { 1, 2}));
        }

        [Test]
        public void should_loop_through_sequence_when_generating_more_items_than_sequence_size()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<int>>()
                .For(x => x.Value).UseSequence(1, 2, 3)
                .Generate(5);

            var numbers = builtItems.Select(i => i.Value).ToArray();

            Assert.That(numbers, Is.EquivalentTo(new[] { 1, 2, 3, 1, 2 }));
        }

        [Test]
        public void should_use_random_values_from_parameters_if_defined()
        {
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<string>>()
                .For(x => x.Value).PickFrom("MyName", "MyName2")
                .Generate(5);

            Assert.That(builtItems.All(x => x.Value == "MyName" || x.Value == "MyName2"));
        }

        [Test]
        public void should_use_random_values_from_collection_if_defined()
        {
            var names = new List<string> { "MyName", "MyName2" };
            var builtItems = new FabricationScope().Setup<SinglePropertyClass<string>>()
                .For(x => x.Value).PickFrom(names)
                .Generate(5);

            Assert.That(builtItems.All(x => x.Value == "MyName" || x.Value == "MyName2"));
        }

        [Test]
        public void should_handle_multiple_rules()
        {
            var builtItem = new FabricationScope().Setup<StringAndInteger>()
                .For(x => x.Name).Use("MyName")
                .For(x => x.Number).Use(12)
                .Generate();

            Assert.That(builtItem.Name, Is.EqualTo("MyName"));
            Assert.That(builtItem.Number, Is.EqualTo(12));
        }

        [Test]
        public void should_fail_if_multiple_rules_are_created_for_the_same_property()
        {
            var options = new FabricationScope().Setup<SinglePropertyClass<string>>()
                .For(x => x.Value).Use("MyName");

            Assert.Throws<AmbiguousRuleException>(() => options.For(x => x.Value).Use("MyOtherName"));
        }

        [Test]
        public void should_handle_rules_for_different_types_with_same_property_name()
        {
            var fabricationScope = new FabricationScope();

            fabricationScope.Setup<SinglePropertyClass<string>>().For(x => x.Value).Use("Scope1Value");
            Assert.DoesNotThrow(() => fabricationScope.Setup<StringAndInteger>().For(x => x.Name).Use("Scope2Value"));
        }

        [Test]
        public void should_set_defaults_for_all_known_types()
        {
            var builtItem = new FabricationScope().Generate<KnownTypes>();

            Assert.That(builtItem, Is.Not.Null);
        }

        [Test]
        public void should_set_defaults_for_all_known_types_with_duplicate_types()
        {
            var builtItem = new FabricationScope().Generate<DuplicateKnownTypes>();

            Assert.That(builtItem, Is.Not.Null);
        }
    }
}